package mgox

import (
	"context"
	"reflect"
	"time"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

type Model[T any] struct {
	DB   *mongo.Database
	Coll *mongo.Collection
}

func (m *Model[T]) getID(data *T) primitive.ObjectID {
	return reflect.ValueOf(data).Elem().FieldByName("ID").Interface().(primitive.ObjectID)
}

func (m *Model[T]) updateID(data *T, id primitive.ObjectID) {
	reflect.ValueOf(data).Elem().FieldByName("ID").Set(reflect.ValueOf(id))
}

func (m *Model[T]) updateCreateAt(data *T) {
	reflect.ValueOf(data).Elem().FieldByName("CreateAt").Set(reflect.ValueOf(time.Now()))
}

func (m *Model[T]) updateUpdateAt(data *T) {
	reflect.ValueOf(data).Elem().FieldByName("UpdateAt").Set(reflect.ValueOf(time.Now()))
}

func (m *Model[T]) Create(ctx context.Context, data *T) error {
	m.updateCreateAt(data)
	m.updateUpdateAt(data)
	rt, err := m.Coll.InsertOne(ctx, data)
	if err != nil {
		return err
	}
	if m.getID(data).IsZero() {
		m.updateID(data, rt.InsertedID.(primitive.ObjectID))
	}
	return nil
}

func (m *Model[T]) Update(ctx context.Context, data *T) error {
	m.updateUpdateAt(data)
	_, err := m.Coll.UpdateOne(ctx, bson.M{"_id": m.getID(data)}, bson.M{"$set": data})
	return err
}

func (m *Model[T]) UpdateMany(ctx context.Context, filter interface{}, update interface{},
	opts ...*options.UpdateOptions) (*mongo.UpdateResult, error) {
	res, err := m.Coll.UpdateMany(ctx, filter, update, opts...)
	return res, err
}

func (m *Model[T]) DeleteOne(ctx context.Context, filter interface{}, opts ...*options.DeleteOptions) error {
	_, err := m.Coll.DeleteOne(ctx, filter, opts...)
	return err
}
func (m *Model[T]) DeleteMany(ctx context.Context, filter interface{}, opts ...*options.DeleteOptions) error {
	_, err := m.Coll.DeleteMany(ctx, filter, opts...)
	return err
}

func (m *Model[T]) DeleteByID(ctx context.Context, id string) error {
	_, err := m.Coll.DeleteOne(ctx, bson.M{"_id": id})
	return err
}

func (m *Model[T]) DeleteByName(ctx context.Context, name string) error {
	_, err := m.Coll.DeleteOne(ctx, bson.M{"name": name})
	return err
}

func (m *Model[T]) FindByID(ctx context.Context, id string) (*T, error) {
	old, err := primitive.ObjectIDFromHex(id)
	if err != nil {
		return nil, err
	}
	return m.FindOne(ctx, bson.M{"_id": old})
}

func (m *Model[T]) FindByName(ctx context.Context, name string) (*T, error) {
	return m.FindOne(ctx, bson.M{"name": name})
}

func (m *Model[T]) FindOne(ctx context.Context, filter any, opts ...*options.FindOneOptions) (*T, error) {
	res := m.Coll.FindOne(ctx, filter, opts...)
	if res.Err() != nil {
		return nil, res.Err()
	}
	var data T
	if err := res.Decode(&data); err != nil {
		return nil, err
	}
	return &data, nil
}

func (m *Model[T]) FindMany(ctx context.Context, filter any, opts ...*options.FindOptions) ([]*T, error) {
	cur, err := m.Coll.Find(ctx, filter, opts...)
	if err != nil {
		return nil, err
	}
	defer func() { _ = cur.Close(ctx) }()
	var v []*T
	err = cur.All(ctx, &v)
	return v, err
}

func (m *Model[T]) FindAll(ctx context.Context) ([]*T, error) {
	return m.FindMany(ctx, bson.M{})
}

func (m *Model[T]) Transaction(ctx context.Context, proc func(ctx context.Context) error) error {

	err := m.DB.Client().UseSession(ctx, func(sc mongo.SessionContext) error {
		if err := sc.StartTransaction(); err != nil {
			return err
		}

		if err := proc(sc); err != nil {
			return sc.AbortTransaction(sc)
		}

		return sc.CommitTransaction(sc)
	})
	return err
}
